namespace Game {
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;

    using UnityEngine;

    public static class Downloader {
        /// <summary>
        /// The priority of download operation
        /// </summary>
        public enum Priority { Immediately, Normal }

        /// <summary>
        /// The state of Downloader
        /// </summary>
        public enum State { Download, Wait }

        // current state
        public static State Curr = State.Wait;
        // filename downloading
        public static string DownloadName = "";
        
        public static IEnumerator OnInit() {
            bundleInfo = CsvFile.Load<int, BundlesInfo>("Bundles.txt", "Id", 1);
            yield return null;

            var files = Utils.GetFiles(Dir.Storage, ".u");
            foreach (var n in files) downloaded.Add(n.Substring(Dir.Storage.Length));

            Assets.Ins.StartCoroutine(DownloadCoroutine());
        }

        public static IEnumerator DownloadAnsyc(int id, System.Action onFinished = null, Priority priority = Priority.Immediately) {
            yield return DownloadAnsyc(new int[] {id}, onFinished, priority);
        }

        public static IEnumerator DownloadAnsyc(int[] ids, Action onFinished = null, Priority priority = Priority.Immediately) {
            if (IsDownloaded(ids)) {
                if (onFinished != null) onFinished.Invoke(); yield break;
            }

            foreach (var id in ids) {
                var info = Find(id);
                foreach (var b in info.Bundles) downQueues[priority].Enqueue(b);
            }
            yield return new WaitUntil(() => { return IsDownloaded(ids); });

            if (onFinished != null) onFinished.Invoke();
        }

        #region Privates
        private static IEnumerator DownloadCoroutine() {
            foreach (Priority p in Enum.GetValues(typeof(Priority))) downQueues[p] = new Queue<string>();

            while (true) {
                string name = null; Priority curr = Priority.Normal;

                foreach (var kv in downQueues) {
                    if (kv.Value.Count > 0) {
                        curr = kv.Key;
                        name = kv.Value.Peek();
                        break;
                    }
                }

                if (!string.IsNullOrEmpty(name)) {
                    DownloadName = name; Curr = State.Download;
                    yield return Download(name, () => {
                        downQueues[curr].Dequeue();
                        downloaded.Add(name);

                        Logger.Info("{0} download ... [OK]", name);
                    });
                    Curr = State.Wait;
                }

                yield return new WaitForEndOfFrame();
            }
        }

        private static IEnumerator Download(string name, System.Action onFinished) {
            if (downloaded.Contains(name)) yield break;

            string url = Dir.UpdateUrl + name;
            WWW www = new WWW(url); yield return www;

            if (www.error != null) {
                Logger.Error("Downloader failed to fetch remote bundle {0}. {1}", url, www.error);
                yield break;
            }

            Utils.WriteBundle2Local(name, www.bytes); www.Dispose();
            if (onFinished != null) onFinished.Invoke();
        } 

        private static bool IsDownloaded(params int[] ids) {
            foreach (var id in ids) {
                var info = Find(id); if (info.Downloaded) continue;
                foreach (var b in info.Bundles) if (!downloaded.Contains(b)) return false;
                info.Downloaded = true;
            }
            return true;
        }

        private class BundlesInfo {
            public int      Id = 0;
            public string[] Bundles = null;
            public int[]    NextIds = null;

            public bool     Downloaded = false;
        }

        private static BundlesInfo Find(int id) {
            BundlesInfo info;
            if (!bundleInfo.TryGetValue(id, out info)) {
                Logger.Error("Downloader find bundles information failed {0}.", id);
                return null;
            }
            return info;
        }
        #endregion

        private static Dictionary<Priority, Queue<string>> downQueues = new Dictionary<Priority, Queue<string>>();
        private static HashSet<string> downloaded = new HashSet<string>();
        private static Dictionary<int, BundlesInfo> bundleInfo = new Dictionary<int, BundlesInfo>();
    }
}
